content management system



content management system CMS

A content management system (CMS) helps companies manage digital content. Whole teams can use these systems to create, edit, organize, and publish content. It acts as a single place to store content and provides automated processes for collaborative digital content management and creation using built-in (or designed) workflows. Different privileges and responsibilities are provided to individuals based on roles. For example, authors can post and save their work, but editors can modify and publish it. Administrators can do all these things as well as grant other people in the organization permission to update or revise content. A CMS helps create and manage websites and website content using minimal technical overhead, so you can make better content instead of acting as a project or traffic manager. By providing an easy and cost-effective solution for content management, a CMS allows companies to manage and distribute their content without investing in a full-time content development team.

Types of content management systems (CMS)

Nearly every CMS is comprised of two parts-the front end and the back end. The front end is the part the user interacts with. It’s how websites are visibly structured and styled. The front end brings HTML, CSS, and JavaScript together to deliver rich, interactive content that’s styled to match your company’s branding. The back end of a CMS is the application that is used to post new content to a website. The process begins by accessing a web interface to easily add, create, and publish content to your CMS’s front end. Rather than knowing HTML, CSS, and JavaScript, you create content in an interface similar to Microsoft Word. The back end then stores this content in the database and publishes it to the front end of the website. Together, these two systems comprise the CMS. They allow you to publish content without understanding web technologies or building your web application from the ground up. The following are some of the different types of content management systems that are available today.

1 Coupled CMS

A coupled CMS is often referred to as a traditional CMS. It offers a fully accessible back end that connects to and modifies a website’s database and publishes content to a styled front end. While a coupled CMS is an all-in-one solution, the main distinction between it and a software-as-a-service (SaaS) CMS is that a coupled CMS requires dedicated web hosting to run. Although web hosting is relatively inexpensive, it's important to remember that a CMS requires installing and maintaining specific technologies to make the software functional. Additionally, a coupled CMS will likely require that an administrator set it up and configure the system installation for ongoing use. WordPress is an example of a coupled CMS, as it offers a complete package for users to install, launch a website, and publish content moving forward.

2 SaaS CMS

A SaaS CMS is also a complete, end-to-end solution, but unlike coupled CMS, SaaS CMS is hosted in the cloud. Which means that it requires no actual setup, installation, or preconfigured web hosting. A SaaS-based CMS is an excellent solution for companies who need a straightforward web presence, as it offers all the capabilities without any of the server or web-hosting overhead. It enables all kinds of users to quickly create websites, manage the content, and distribute it through digital channels.

3 Decoupled CMS

In a decoupled CMS, the presentation part of the website is “decoupled” from the back end. The delivery system sits between the presentation of the website and accesses the back end through an application programming interface (API). A decoupled CMS is an advanced solution that offers greater flexibility to interact with the content created in the back end. For example, suppose an organization wants to use its library of content for a new purpose, such as mobile applications. In that case, a decoupled CMS is an appealing solution because it supports multiple, adaptable applications on the front end while keeping your content and information consistent in the back end.

4 Headless CMS

A headless CMS has only a back-end system that accesses a database and stores content with a custom-built, front-end web application. It offers greater flexibility than a decoupled CMS, but it also requires considerably more work than any other option. A headless CMS also usually requires a developer to design, create, and connect a front-end application. A headless CMS is a good solution for organizations that need complete control and flexibility over how their content is accessed. It provides content storage and organizational capabilities while allowing for a custom application on the front end-whether a website, a mobile app, or some other front end.

Core features to a content management system

Finding out what features of a CMS are critical to your business might start with what features are core to a content management system. Leveraging side-by-side comparisons of content management solutions and products can be helpful to your decision-making process. Still, the core question is, what features are core to any solution?

User roles and role-based content management

There are different types of roles within any Content Management System. Understanding how they work together is essential to giving your users access to perform their duties and access appropriate digital content. These range from typical organization roles to application management roles, task and feature comparison by application roles, to the resource (permissions) type roles, deciding what users can see and do with the content, including documents, sites, or templates.

Digital asset management

Content Management systems like Oracle’s offer powerful capabilities to manage all your digital assets for use in different marketing channels, including websites, marketing materials, email campaigns, online stores, paid search, and blogs. It provides a centralized content hub for all your assets, where you can organize them into repositories and collections and create rules and workflow to define how the content can be used and where. Creating asset types to define what information you need to collect when users create assets is critical to any CMS. Digital asset types will define the custom attributes required for your digital assets (files, images, and videos).

Manage content in the cloud

As with ours, it is critical to move your content management to the cloud, centralizing your content all in one place and making it accessible anywhere. You can group your files in folders to perform everyday file management operations, including copy, move, delete, in much the same way as on your local system and architecture. Since all the content files reside in the cloud, users will access them wherever they go, including mobile devices.

Content collaboration

With all your organizations’ content in the cloud, it will be easy to share assets or folders to collaborate with others both inside and outside your organization. Everyone you share content with will have access to the latest information-wherever they are, whenever they need it. Sharing and recording content coloration will enable the user to monitor how and when each shared item was accessed.

Build websites

With content management solutions, you can rapidly build and publish marketing, help, and community websites-from concept to launch-with engaging online experiences. The website building process is completely integrated across content, collaboration, and design are via a single authoring and publishing environment. Cloud-based solutions make it easy to get started quickly by using out-of-the-box templates, drag-and-drop components (WYSIWYG), sample page layouts, and site themes to assemble a website from predefined building blocks. Or your developers can create custom designs, including templates, themes, or components to create unique online experiences.

What to consider before evaluating if a CMS is right for your business


A CMS makes content more findable by addressing six key areas in content management. Content governance Are there enforced policies and procedures in place for creating and managing content? Information architecture for tagging Is current and new content categorized and tagged in a way that everyone understands? Business process for content management Are workflows standardized and automated? User experience for content How are key stakeholders-from employees to customers-finding the information they need? Technology and applications Is the existing technology being used properly? What improvements or new systems would streamline information flow with optimal security? Do you have a cloud-based content management system? Business value of a CMS Can your data and content be managed more efficiently and securely? Are applicable regulatory guidelines being adhered to?

Using a content management system for your website


A content management system helps you create, manage, and publish content on the web. It also helps keep content organized and accessible so it can be used and repurposed effectively. There are various kinds of content management systems available-from cloud-based to a headless CMS-to meet every audience need. Not only does it give you a way to store and manage all your information in a single, easily accessible database, but it also does the following:

How does a CMS help to build websites?


Basically, a CMS platform takes care of all the technical details around building and managing a website. For example, to post content on the web, such as a blog post, you need to start with an HTML file. HTML allows you to structure your written content so that a web browser can read it. It also allows you to embed images and videos into your content and link to other HTML documents. Once the content is created and structured, you can change the look and feel of it by using cascading style sheets (CSS). With CSS, you can change the font, color, and size of every element on your page to achieve the desired styling. Once completed, you upload the HTML and CSS files-along with any image and video files-to a web server where your new website is accessible to anyone. While this process may not seem terribly difficult, it’s not a very efficient way to create documents and share them online. And most organizations don’t have the IT resources to devote to this task.

Advanced CMS technologies


HTML and CSS are OK for creating simple, readable documents, but they are limited when building websites that offer extensive capabilities and functionality-the kind of modern website most companies need to be successful. For example, to add interactive features and more nuanced functionality to your website, you need to use JavaScript-the programming language of the web. HTML and CSS are relatively straightforward codebases used to create simple documents. JavaScript is a dedicated programming language not dissimilar from the languages used to develop smartphone or desktop applications. Suppose you plan to migrate your blog site into a media website that serves thousands of visitors per month and posts several pieces of content per day. In that case, you need to start incorporating database technologies to house and access all your content. You need a database with its own programming language as well as another programming language to pass information between the database and the part of the website that users access and navigate. Just like that, things got complicated rather quickly. And that's where a CMS comes in.

Content management system (CMS) benefits

The demand for digital marketing experiences is growing, and shows no signs of letting up. Today’s modern consumers are demanding omnichannel solutions and frictionless experiences. Digital technology provides endless ways for companies to engage with their customers and stay connected. So it’s no wonder that CMS market is expected to grow from $36 billion in 2018 to $123.5 billion by 2026. Companies recognize how a content management system can help them leverage the full value of their content by streamlining the content creation and distribution process. Marketing and sales teams who invest in a CMS can expect to gain four key benefits as they work to get their message into the market and improve customer experiences.

1 Stronger collaboration and organization

With a CMS, multiple content marketing team members can contribute and help publish content. With its workflow management, content storage, and scheduling capabilities, the system helps them keep everything organized. Browser-based CMSs can be accessed from anywhere, and teammates in different locations can collaborate on content projects in the same system and on the most current version. Team members can all access the same content since it’s stored in the same place. There’s no need to send multiple files to multiple people-and no headaches from trying to control multiple versions.

2 User friendly

With a CMS, users don’t have to learn HTML or CSS. No matter their skill level, users across the organization can create and publish content. A content management system makes it easy to upload content onto web pages and update it through a content editor.

3 SEO and content optimization tools and plug-ins

Want to improve your search engine optimization (SEO) and drive traffic to your site? With a CMS, you can add plug-ins and tools to increase your search ranking. These tools can include options within the front-end interface to add web page titles, meta descriptions, and alt tags.

4 More time to spend on content

With a straightforward CMS system, better organization, and tools and plug-ins to help SEO, Content creators can concentrate on producing quality content. Marketing teams don't have to work with multiple files or code. They can share their work with colleagues easily, even if those colleagues are miles away. They have time to refine copy, tweak imagery, produce more video content, or conduct A/B testing on different subject lines, offers, CTAs, and formats.

PHP Building Your First Simple CMS

The Magic of PHP + MySQL

It's safe to say that nearly every website that's up-to-date these days is using some form of content management system (CMS). While there are a ton of great free options that provide us with a CMS to power a website (WordPress, Drupal, etc.), it doesn't hurt to peek under the hood and get a feel for how these systems work. To get our feet wet as back-end developers, we'll be creating a simple PHP class that will: Download Files This class is intended to give you a feel for how PHP and MySQL interact together, and to show the basics of a CMS. I'll be skipping explanations of some of the very basic programming stuff, so if at any point you feel lost, checkout the course Diving into PHP and give yourself a crash-course in PHP. I'll try not to lose anyone, though, I promise.

Building the Class

Our first step is to simply lay out the class in a file named 'simpleCMS.php' so we have a road map to work with. <?php class simpleCMS { var $host; var $username; var $password; var $table; public function display_public() { } public function display_admin() { } public function write() { } public function connect() { } private function buildDB() { } } ?> As you can see, we're creating one class with four variables and five methods. I've opted to use PHP's object-oriented approach because it makes for cleaner code in large projects, and, in my opinion, it's just good practice.

The Variables

In this case, all four variables are for connecting to the database: $host, $username, $password, and $table provide a path and access to our database on the server. For now, we'll leave those empty and move on to our database, which is constructed by the method buildDB().

Build the Database

private function buildDB() { $sql = <<<MySQL_QUERY CREATE TABLE IF NOT EXISTS testDB ( title VARCHAR(150), bodytext TEXT, created VARCHAR(100) ) MySQL_QUERY; return mysql_query($sql); } This function runs a MySQL command that checks the database to see if testDB exists. If so, it simply passes along a notification of success; if not, it creates our table and assigns three columns to hold data.

Connect to the Database

Now that we have a function to build our table, let's create the function that will connect to our database. public function connect() { mysql_connect($this->host,$this->username,$this->password) or die("Could not connect. " . mysql_error()); mysql_select_db($this->table) or die("Could not select database. " . mysql_error()); return $this->buildDB(); } We call mysql_connect() to hook into our database, and then mysql_select_db() to make sure we save our data in the right place. Both of these functions are accompanied by the die() command, which essentially says, "in the event that this function fails, stop execution of this script and display a message." Our connect() function connects to the database and gets us pointed in the right direction, then runs our buildDB() function. Remember the grammatically awkward "IF NOT EXISTS" part of our MySQL command? Because we're going to run this function every time the page is loaded, we have to make sure we're not overwriting our database with every function call, and that's exactly what that phrase requires.

Build the Form

So, we've got a database. Now we just need to put stuff in it! public function display_admin() { return <<<ADMIN_FORM <form action="{$_SERVER['PHP_SELF']}" method="post"> <label for="title">Title:</label> <input name="title" type="text" maxlength="150" /> <label for="bodytext">Body Text:</label> <textarea name="bodytext"></textarea> <input type="submit" value="Create This Entry!" /> </form> ADMIN_FORM; } Again, this is a very simple function. When called, it simply returns the HTML markup to create our form. You'll notice, however, in the action attribute of the form element, that I've used the variable $_SERVER['PHP_SELF']. This is, essentially, a shortcut that references the file you're currently using (in our case, it's display.php). This is useful if you'll be reusing your code across a site and don't necessarily want to rewrite this function for each page. I'm also going to take a second right now to talk about the method I'm using to return the HTML. It's a format used in PHP called HEREDOC syntax, and I love it. The primary advantage of HEREDOC is that it allows you to include formatting in your output. This is extraordinarily useful for folks like me who take issue with cluttered source code. You can read more about HEREDOC syntax and its ilk in the PHP manual.

Saving the Data to the Database

Our form will allow us to input information, so how do we save it? That's where our write() method comes in. public function write($p) { if ( $p['title'] ) $title = mysql_real_escape_string($p['title']); if ( $p['bodytext']) $bodytext = mysql_real_escape_string($p['bodytext']); if ( $title && $bodytext ) { $created = time(); $sql = "INSERT INTO testDB VALUES('$title','$bodytext','$created')"; return mysql_query($sql); } else { return false; } } Let's start with the function call itself — we're passing a variable to this one, which we haven't done so far. Our variable $p is going to hold the information sent from our form via the post method. Once inside the function, we start with a conditional statement that's checking to see if the the title value was set in the form before it was submitted, and if so, we're setting our $title variable to the $_POST['title'] value (NOTE: we're using the function mysql_real_escape_string() as a precaution against potentially dangerous input, which is important to keep in mind when you're building anything that will allow users to input information). If $_POST['title'] wasn't set, we skip this line, leaving the $title variable unset. This process is repeated for our second input, and then both variables are checked to make sure nothing is blank before saving to the database. If both variables are set, we then set the $created variable with the current Unix timestamp, which we'll use to sort our entries chronologically when we view them in the future. We now have three variables, and because we've run checks, we know that all three variables are not empty. Now we can write our MySQL query that will save the entry in the database!

Displaying the Information from the Database

Now that we have the means to put information into our database, we need to create a way to get that information back out. This is where display_public() comes in. This is by far the most complex of our methods, so let's really take our time and figure out what's going on inside. public function display_public() { $q = "SELECT * FROM testDB ORDER BY created DESC LIMIT 3"; $r = mysql_query($q); if ( $r !== false && mysql_num_rows($r) > 0 ) { while ( $a = mysql_fetch_assoc($r) ) { $title = stripslashes($a['title']); $bodytext = stripslashes($a['bodytext']); $entry_display .= <<<ENTRY_DISPLAY <h2>$title</h2> <p> $bodytext </p> ENTRY_DISPLAY; } } else { $entry_display = <<<ENTRY_DISPLAY <h2>This Page Is Under Construction</h2> <p> No entries have been made on this page. Please check back soon, or click the link below to add an entry! </p> ENTRY_DISPLAY; } $entry_display .= <<<ADMIN_OPTION <p> <a href="{$_SERVER['PHP_SELF']}?admin=1">Add a New Entry</a> </p> ADMIN_OPTION; return $entry_display; } The first thing to note when reading from a database is the way PHP and MySQL interact with each other. First, we ask the database a question (query), to which it replies with a result (resource). However, this result isn't really useful until we've decoded it using one of several methods that "fetch," or organize, the information that's contained inside into a usable form (array). Our very first action in the above function is to set up our query in the variable $q. The asterisk (*) operator in MySQL means "everything," so our query is asking the database to select everything from entries in the table testDB in reverse chronological order, limited to the first three entries returned. Now that the query is defined, we send it to the database using the function mysql_query(). The resulting resource is stored in the variable $r. This is where it gets a bit tricky. We now run a conditional statement that says, "IF mysql_query() didn't fail, AND IF the number of entries returned was greater than zero, process the result, OR ELSE display a default message." If $r contains entries from the database, we now have to "fetch" that data. Information from the database is returned as an array, which is organized similarly to the database table itself. The function mysql_fetch_assoc() will take the resource and break each entry into an associative array (this means that when we save the result of mysql_fetch_assoc() into the variable $a, the data from the entry will be accessible by the column names in the database, i.e. $a['title']). However, mysql_fetch_assoc() only gives us one entry at a time. To get all of the returned entries, we have to use a while loop. Essentially, we're saying, "WHILE $r has values we haven't used yet, get the next entry in line and do the following actions with it." In this case, we're going to check the entry to make sure that data was returned, then remove the slashes that were added when we saved the information to the database using stripslashes(). After that, we simply wrap the variables in some HTML and, voila! we've got screen-ready content! As a final step, the code adds a link to the bottom that allows users to add an entry. It's worth noting the use of the ".=" operator used in the while loop and when creating the "Add a New Entry" link; a function can only return one variable, so we need to append the new information to the existing variable. If we just used the equals sign ("="), we would overwrite existing data and end up with just a link to the form and no content. So, you've now written your first CMS class! You can easily write and retrieve data to and from a database. All that's left to do is to try it out!

Using the Class

To use our class, we need to create a separate file. I'm going to call it "display.php", which I'll save in the main web folder, with our class saved as "simpleCMS.php" in a folder called "_class" within the main folder. To start, we just set up a document with plain ol' HTML. <!DOCTYPE html> <html lang="en"> <head> <title>SimpleCMS</title> </head> <body> </body> </html> To use our class, we just have to insert a little PHP between the body tags: <?php include_once('_class/simpleCMS.php'); $obj = new simpleCMS(); $obj->host = 'database.host.net'; $obj->username = 'DB1234567'; $obj->password = 'DBpassword'; $obj->table = 'DB1234567'; $obj->connect(); if ( $_POST ) $obj->write($_POST); echo ( $_GET['admin'] == 1 ) ? $obj->display_admin() : $obj->display_public(); ?> First and foremost, we have to include the class using the include_once() function. Then, we have to instantiate our object so that our code knows what's going on. Third, we set all of those variables we talked about toward the beginning of this tutorial. You'll have to replace all of those values with the information you get from your own server or hosting company. And fourth, we connect to our database using the connect() method. After we've connected to the database, we check to see if any $_POST information exists. This is because we're using the same file for input, processing, and display of information. If anything was passed via $_POST, we run the write() function to validate it and save it to the database. Then, we use some shorthand trickery to run a conditional statement. In essence, we're saying, "IF $_GET['admin'] is set to 1, then show the form using display_admin(), OR ELSE show me the stored entries using display_public()." And that's it! Once you get a feel for it, this sort of basic programming will allow you to start exercising total control over websites you build, whether you decide to really dig in and build your own CMS framework or just improve an existing CMS by, say, writing a WordPress plugin. Really, when it comes to modern web design, you should have at least some understanding of how things are working behind the curtain—understanding how a site works will better enable you to design sites that have a more fluid integration of form and function. And besides, adding PHP and MySQL to your curriculum vitae definitely won't hurt your credibility... Download Files

Build a CMS with PHP and MySQL

Learn how to build a complete content management system using PHP and MySQL in just a few hours. Full code download included. Building a content management system can seem like a daunting task to the novice PHP developer. However, it needn't be that difficult. In this tutorial I'll show you how to build a basic, but fully functional, CMS from scratch in just a few hours. Yes, it can be done! Along the way, you'll learn how to create MySQL databases and tables; how to work with PHP objects, constants, includes, sessions, and other features; how to separate business logic from presentation; how to make your PHP code more secure, and much more! Before you begin, check out the finished product by clicking the View Demo button above. (For security reasons this demo is read-only, so you can't add, change or delete articles.) You can also click the Download Code button above to download the complete PHP code for the CMS, so you can run it on your own server. For this tutorial, you'll need to have the Apache web server with PHP installed, as well as the MySQL database server running on your computer. Setting all this up is beyond the scope of the tutorial, but a really easy way to do it is simply to install XAMPP on your computer.

What will the CMS do ?

Our first job is to work out exactly what we want our CMS to do. The CMS will have the following features: Front end: The homepage, listing the 5 most recent articles The article listing page, listing all articles The "view article" page, letting visitors see a single article Back end: Admin login/logout List all articles Add a new article Edit an existing article Delete an existing article Each article will have an associated headline, summary, and publication date.

Planning it out

Here are the steps we'll need to follow to create our CMS: Create the database Create the articles database table Make a configuration file Build the Article class Write the front-end index.php script Write the back-end admin.php script Create the front-end templates Create the back-end templates Create the stylesheet and logo image This page contains all the code for the CMS, ready for you to copy and paste into your own files. If you don't want to create the files yourself, simply download the finished zip file, which contains all the code files and folders. Ready? Grab a cup of tea, and let's get coding!

Step 1: Create the database

The first thing we need to do is create a MySQL database to store our content. You can do this as follows: Run the mysql client program Open a terminal window and enter the following: mysql -u username -p Then enter your MySQL password when prompted. username should be a user that has permission to create databases. If you're working on a development server, such as your own computer, then you can use the root user for this, to save having to create a new user. Create the database At the mysql> prompt, type: create database cms; Then press Enter. Quit the mysql client program At the mysql> prompt, type: exit Then press Enter. That's it! You've now created a new, empty database, into which you can put your database tables and content. Some web server setups let you create databases via a web-based tool such as cPanel or Plesk (in fact sometimes this is the only way to create MySQL databases). If you're not sure what to do on your server, ask your tech support team for help.

Step 2: Create the articles database table

Our simple CMS has just one database table: articles. This, as you'd imagine, holds all of the articles in the system. Let's create the schema for the table. A table's schema describes the types of data that the table can hold, as well as other information about the table. Create a text file called tables.sql somewhere on your hard drive. Add the following code to the file: DROP TABLE IF EXISTS articles; CREATE TABLE articles ( id smallint unsigned NOT NULL auto_increment, publicationDate date NOT NULL, # When the article was published title varchar(255) NOT NULL, # Full title of the article summary text NOT NULL, # A short summary of the article content mediumtext NOT NULL, # The HTML content of the article PRIMARY KEY (id) ); The above code defines the schema for the articles table. It's written in SQL, the language used to create and manipulate databases in MySQL (and most other database systems). Let's break the above code down a little: Create the articles table DROP TABLE IF EXISTS articles removes any existing articles table (and data - be careful!) if it already exists. We do this because we can't define a table with the same name as an existing table. CREATE TABLE articles ( ) creates the new articles table. The stuff inside the parentheses defines the structure of the data within the table, explained below... Give each article a unique ID We're now ready to define our table structure. A table consists of a number of fields (also called columns). Each field holds a specific type of information about each article. First, we create an id field. This has a smallint unsigned (unsigned small integer) data type, which means it can hold whole numbers from 0 to 65,535. This lets our CMS hold up to 65,535 articles. We also specify the NOT NULL attribute, which means the field can't be empty (null) - this makes life easier for us. We also add the auto_increment attribute, which tells MySQL to assign a new, unique value to an article's id field when the article record is created. So the first article will have an id of 1, the second will have an id of 2, and so on. We'll use this unique value as a handle to refer to the article that we want to display or edit in the CMS. Add the publicationDate field The next line creates the publicationDate field, which stores the date that each article was published. This field has a data type of date, which means it can store date values. Add the title field Next we create the title field to hold each article's title. It has a data type of varchar(255), which means it can store a string of up to 255 characters. Add the summary and content fields The last 2 fields, summary and content, hold a short summary of the article and the article's HTML content respectively. summary has a text data type (which can hold up to 65,535 characters) and content has a mediumtext data type (which can hold up to 16,777,215 characters). Add the primary key The last line inside the CREATE TABLE statement defines a key for the table. A key is also called an index, and in simple terms it makes it quicker to find data in the table, at the expense of some extra storage space. We make the id field a PRIMARY KEY. Each table can only have a single PRIMARY KEY; this is the key that uniquely identifies each record in the table. In addition, by adding this key, MySQL can retrieve an article based on its ID very quickly. Now that we've created our table schema, we need to load it into MySQL to create the table itself. The easiest way to do this is to open up a terminal window and change to the folder containing your tables.sql file, then run this command: mysql -u username -p cms < tables.sql ...where username is your MySQL username. cms is the name of the database that you created in Step 1. Enter your password when prompted. MySQL then loads and runs the code in your tables.sql file, creating the articles table inside the cms database. You can also use a web-based admin tool such as phpMyAdmin to run your tables.sql code and create the table. phpMyAdmin comes pre-installed with most web hosting accounts.

Step 3: Make a configuration file

Now that you've created your database, you're ready to start writing your PHP code. Let's start by creating a configuration file to store various useful settings for our CMS. This file will be used by all the script files in our CMS. First, create a cms folder somewhere in the local website on your computer, to hold all the files relating to the CMS. If you're running XAMPP then the local website will be in an htdocs folder inside your XAMPP folder. Or, if you prefer, you can create a brand new website just for your CMS, and put all the files in that new website's document root folder. Inside the cms folder, create a file called config.php with the following code: <?php ini_set( "display_errors", true ); date_default_timezone_set( "Australia/Sydney" ); // http://www.php.net/manual/en/timezones.php define( "DB_DSN", "mysql:host=localhost;dbname=cms" ); define( "DB_USERNAME", "username" ); define( "DB_PASSWORD", "password" ); define( "CLASS_PATH", "classes" ); define( "TEMPLATE_PATH", "templates" ); define( "HOMEPAGE_NUM_ARTICLES", 5 ); define( "ADMIN_USERNAME", "admin" ); define( "ADMIN_PASSWORD", "mypass" ); require( CLASS_PATH . "/Article.php" ); function handleException( $exception ) { echo "Sorry, a problem occurred. Please try later."; error_log( $exception->getMessage() ); } set_exception_handler( 'handleException' ); ?> Let's break this file down: Display errors in the browser The ini_set() line causes error messages to be displayed in the browser. This is good for debugging, but it should be set to false on a live site since it can be a security risk. Set the timezone As our CMS will use PHP's date() function, we need to tell PHP our server's timezone (otherwise PHP generates a warning message). Mine is set to "Australia/Sydney" - change this value to your local timezone. Set the database access details Next we define a constant, DB_DSN, that tells PHP where to find our MySQL database. Make sure the dbname parameter matches the name of your CMS database (cms in this case). We also store the MySQL username and password that are used to access the CMS database in the constants DB_USERNAME and DB_PASSWORD. Set these values to your MySQL username and password. Set the paths We set 2 path names in our config file: CLASS_PATH, which is the path to the class files, and TEMPLATE_PATH, which is where our script should look for the HTML template files. Both these paths are relative to our top-level cms folder. Set the number of articles to display on the homepage HOMEPAGE_NUM_ARTICLES controls the maximum number of article headlines to display on the site homepage. We've set this to 5 initially, but if you want more or less articles, just change this value. Set the admin username and password The ADMIN_USERNAME and ADMIN_PASSWORD constants contain the login details for the CMS admin user. Again, you'll want to change these to your own values. Include the Article class Since the Article class file - which we'll create next - is needed by all scripts in our application, we include it here. Create an exception handler Finally, we define handleException(), a simple function to handle any PHP exceptions that might be raised as our code runs. The function displays a generic error message, and logs the actual exception message to the web server's error log. In particular, this function improves security by handling any PDO exceptions that might otherwise display the database username and password in the page. Once we've defined handleException(), we set it as the exception handler by calling PHP's set_exception_handler() function. This exception handler is a bit of a quick and dirty shortcut to keep the tutorial as simple as possible. The "proper" way to handle exceptions is to wrap all the PDO calls within Article.php in try ... catch blocks.

Security note

In a live server environment it'd be a good idea to place config.php somewhere outside your website's document root, since it contains usernames and passwords. While it's not usually possible to read the source code of a PHP script via the browser, it does happen sometimes if the web server is misconfigured. You could also use hash() to make a hash from your admin password, and store the hash in config.php instead of the plaintext password. Then, at login time, you can hash() the entered password and see if it matches the hash in config.php.

Step 4: Build the Article class

about Class You're now ready to build the Article PHP class. This is the only class in our CMS, and it handles the nitty-gritty of storing articles in the database, as well as retrieving articles from the database. Once we've built this class, it will be really easy for our other CMS scripts to create, update, retrieve and delete articles. Inside your cms folder, create a classes folder. Inside that classes folder, create a new file called Article.php, and put the following code into it: <?php //Class to handle articles class Article { // Properties // @var int The article ID from the database public $id = null; // @var int When the article was published public $publicationDate = null; // @var string Full title of the article public $title = null; // @var string A short summary of the article public $summary = null; // @var string The HTML content of the article public $content = null; // Sets the object's properties, @param assoc The property values public function __construct( $data=array() ) { if ( isset( $data['id'] ) ) $this--->id = (int) $data['id']; if ( isset( $data['publicationDate'] ) ) $this->publicationDate = (int) $data['publicationDate']; if ( isset( $data['title'] ) ) $this->title = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['title'] ); if ( isset( $data['summary'] ) ) $this->summary = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['summary'] ); if ( isset( $data['content'] ) ) $this->content = $data['content']; } // Sets the object's properties using the edit form post values, @param assoc The form post values public function storeFormValues ( $params ) { // Store all the parameters $this->__construct( $params ); // Parse and store the publication date if ( isset($params['publicationDate']) ) { $publicationDate = explode ( '-', $params['publicationDate'] ); if ( count($publicationDate) == 3 ) { list ( $y, $m, $d ) = $publicationDate; $this->publicationDate = mktime ( 0, 0, 0, $m, $d, $y ); } } } // @return Article|false The article object, or false if the record was not found public static function getById( $id ) { $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD ); $sql = "SELECT *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM articles WHERE id = :id"; $st = $conn->prepare( $sql ); $st->bindValue( ":id", $id, PDO::PARAM_INT ); $st->execute(); $row = $st->fetch(); $conn = null; if ( $row ) return new Article( $row ); } /** * Returns all (or a range of) Article objects in the DB * * @param int Optional The number of rows to return (default=all) * @param string Optional column by which to order the articles (default="publicationDate DESC") * @return Array|false A two-element array : results => array, a list of Article objects; totalRows => Total number of articles */ public static function getList( $numRows=1000000, $order="publicationDate DESC" ) { $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD ); $sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM articles ORDER BY " . mysql_escape_string($order) . " LIMIT :numRows"; $st = $conn->prepare( $sql ); $st->bindValue( ":numRows", $numRows, PDO::PARAM_INT ); $st->execute(); $list = array(); while ( $row = $st->fetch() ) { $article = new Article( $row ); $list[] = $article; } // Now get the total number of articles that matched the criteria $sql = "SELECT FOUND_ROWS() AS totalRows"; $totalRows = $conn->query( $sql )->fetch(); $conn = null; return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) ); } // Inserts the current Article object into the database, and sets its ID property. public function insert() { // Does the Article object already have an ID? if ( !is_null( $this->id ) ) trigger_error ( "Article::insert(): Attempt to insert an Article object that already has its ID property set (to $this->id).", E_USER_ERROR ); // Insert the Article $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD ); $sql = "INSERT INTO articles ( publicationDate, title, summary, content ) VALUES ( FROM_UNIXTIME(:publicationDate), :title, :summary, :content )"; $st = $conn->prepare ( $sql ); $st->bindValue( ":publicationDate", $this->publicationDate, PDO::PARAM_INT ); $st->bindValue( ":title", $this->title, PDO::PARAM_STR ); $st->bindValue( ":summary", $this->summary, PDO::PARAM_STR ); $st->bindValue( ":content", $this->content, PDO::PARAM_STR ); $st->execute(); $this->id = $conn->lastInsertId(); $conn = null; } // Updates the current Article object in the database. public function update() { // Does the Article object have an ID? if ( is_null( $this->id ) ) trigger_error ( "Article::update(): Attempt to update an Article object that does not have its ID property set.", E_USER_ERROR ); // Update the Article $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD ); $sql = "UPDATE articles SET publicationDate=FROM_UNIXTIME(:publicationDate), title=:title, summary=:summary, content=:content WHERE id = :id"; $st = $conn->prepare ( $sql ); $st->bindValue( ":publicationDate", $this->publicationDate, PDO::PARAM_INT ); $st->bindValue( ":title", $this->title, PDO::PARAM_STR ); $st->bindValue( ":summary", $this->summary, PDO::PARAM_STR ); $st->bindValue( ":content", $this->content, PDO::PARAM_STR ); $st->bindValue( ":id", $this->id, PDO::PARAM_INT ); $st->execute(); $conn = null; } // Deletes the current Article object from the database. public function delete() { // Does the Article object have an ID? if ( is_null( $this->id ) ) trigger_error ( "Article::delete(): Attempt to delete an Article object that does not have its ID property set.", E_USER_ERROR ); // Delete the Article $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD ); $st = $conn->prepare ( "DELETE FROM articles WHERE id = :id LIMIT 1" ); $st->bindValue( ":id", $this->id, PDO::PARAM_INT ); $st->execute(); $conn = null; } } ?> This file is quite long, but it's fairly simple stuff when you break it down. Let's take a look at each section of the code:

1. The class definition and properties

First, we begin to define our Article class with the code: class Article { Everything after these lines of code - up until the closing brace at the end of the file - contains the code that makes up the Article class. After starting our class definition, we declare the properties of the class: $id, $publicationDate, and so on. Each Article object that we create will store its article data in these properties. You can see that the property names mirror the field names in our articles database table. Technically, this type of class - which contains properties that map directly to the corresponding database fields, as well as methods for storing and retrieving records from the database - follows an object-oriented design pattern known as active record.

2. The constructor

Next we create the class methods. These are functions that are tied to the class, as well as to objects created from the class. Our main code can call these methods in order to manipulate the data in the Article objects. The first method, __construct(), is the constructor. This is a special method that is called automatically by the PHP engine whenever a new Article object is created. Our constructor takes an optional $data array containing the data to put into the new object's properties. We then populate those properties within the body of the constructor. This gives us a handy way to create and populate an object in one go. $this->propertyName means: "The property of this object that has the name "$propertyName". You'll notice that the method filters the data before it stores them in the properties. The id and publicationDate properties are cast to integers using (int), since these values should always be integers. The title and summary are filtered using a regular expression to only allow a certain range of characters. It's good security practice to filter data on input like this, only allowing acceptable values and characters through. We don't filter the content property, however. Why? Well, the administrator will probably want to use a wide range of characters, as well as HTML markup, in the article content. If we restricted the range of allowed characters in the content then we would limit the usefulness of the CMS for the administrator. Normally this could be a security loophole, since a user could insert malicious JavaScript and other nasty stuff in the content. However, since we presumably trust our site administrator - who is the only person allowed to create the content - this is an acceptable tradeoff in this case. If you were dealing with user-generated content, such as comments or forum posts, then you would want to be more careful, and only allow "safe" HTML to be used. A really great tool for this is HTML Purifier, which thoroughly analyses HTML input and removes all potentially malicious code. PHP security is a big MySQLCMStopic, and beyond the scope of this tutorial. If you'd like to find out more then start with Terry Chay's excellent post, Filter Input-Escape Output: Security Principle and Practice. Also see the Wikipedia entries on secure input/output handling, XSS, CSRF, SQL injection, and session fixation.

3. storeFormValues()

Our next method, storeFormValues(), is similar to the constructor in that it stores a supplied array of data in the object's properties. The main difference is that storeFormValues() can handle data in the format that is submitted via our New Article and Edit Article forms (which we'll create later). In particular, it can handle publication dates in the format YYYY-MM-DD, converting the date into the UNIX timestamp format suitable for storing in the object. UNIX timestamps are integer values representing the number of seconds between midnight on 1 Jan, 1970 and the date/time in question. I generally like to handle dates and times as UNIX timestamps in PHP, since they're easy to store and manipulate. The purpose of this method is simply to make it easy for our admin scripts to store the data submitted by the forms. All they have to do is call storeFormValues(), passing in the array of form data. All of the members (that is, the properties and methods) of our Article class have the public keyword before their names, which means that they're available to code outside the class. You can also create private members (which can only be used by the class itself) and protected members (which can be used by the class and any of its subclasses). Don't worry, I'll be covering all this in a later tutorial!

4. getById()

Now we come to the methods that actually access the MySQL database. The first of these, getById(), accepts an article ID argument ($id), then retrieves the article record with that ID from the articles table, and stores it in a new Article object. Usually, when you call a method, you first create or retrieve an object, then call the method on that object. However, since this method returns a new Article object, it would be helpful if the method could be called directly by our calling code, and not via an existing object. Otherwise, we would have to create a new dummy object each time we wanted to call the method and retrieve an article. To enable our method to be called without needing an object, we add the static keyword to the method definition. This allows the method to be called directly without specifying an object: public static function getById( $id ) { The method itself uses PDO to connect to the database, retrieve the article record using a SELECT SQL statement, and store the article data in a new Article object, which is then returned to the calling code. PDO - PHP Data Objects - is an object-oriented library built into PHP that makes it easy for PHP scripts to talk to databases. Let's break this method down: Connect to the database $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD ); This makes a connection to the MySQL database using the login details from the config.php file, and stores the resulting connection handle in $conn. This handle is used by the remaining code in the method to talk to the database. Retrieve the article record $sql = "SELECT *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM articles WHERE id = :id"; $st = $conn->prepare( $sql ); $st->bindValue( ":id", $id, PDO::PARAM_INT ); $st->execute(); $row = $st->fetch(); Our SELECT statement retrieves all fields (*) from the record in the articles table that matches the given id field. It also retrieves the publicationDate field in UNIX timestamp format instead of the default MySQL date format, so we can store it easily in our object. Rather than placing our $id parameter directly inside the SELECT string, which can be a security risk, we instead use :id. This is known as a placeholder. In a minute, we'll call a PDO method to bind our $id value to this placeholder. Once we've stored our SELECT statement in a string, we prepare the statement by calling $conn->prepare(), storing the resulting statement handle in a $st variable. Prepared statements are a feature of most databases; they allow your database calls to be faster and more secure. We now bind the value of our $id variable - that is, the ID of the article we want to retrieve - to our :id placeholder by calling the bindValue() method. We pass in the placeholder name; the value to bind to it; and the value's data type (integer in this case) so that PDO knows how to correctly escape the value. Lastly, we call execute() to run the query, then we use fetch() to retrieve the resulting record as an associative array of field names and corresponding field values, which we store in the $row variable. Close the connection $conn = null; Since we no longer need our connection, we close it by assigning null to the $conn variable. It's a good idea to close database connections as soon as possible to free up memory on the server. Return the new Article object if ( $row ) return new Article( $row ); } The last thing our method needs to do is create a new Article object that stores the record returned from the database, and return this object to the calling code. First it checks that the returned value from the fetch() call, $row, does in fact contain data. If it does then it creates a new Article object, passing in $row as it does so. Remember that this calls our constructor that we created earlier, which populates the object with the data contained in the $row array. We then return this new object, and our work here is done.

5. getList()

Our next method, getList(), is similar in many ways to getById(). The main difference, as you might imagine, is that it can retrieve many articles at once, rather than just 1 article. It's used whenever we need to display a list of articles to the user or administrator. getList() accepts 2 optional arguments:
$numRows
The maximum number of articles to retrieve. We default this value to 1,000,000 (i.e. effectively all articles). This parameter allows us to display, say, just the first 5 articles on the site homepage.
$order
The sort order to use when returning the articles. We default this to "publicationDate DESC", which means "sort by publication date, newest first".
Much of this method's code is similar to getById(). Let's look at a few lines of interest: $sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM articles ORDER BY " . mysql_escape_string($order) . " LIMIT :numRows"; Our query is a bit more complex than last time. First, notice that there's no WHERE clause this time; this is because we want to retrieve all articles, rather than an article that matches a specific ID. We've added an ORDER BY clause to sort the returned records by the specified sort order. We've also added a LIMIT clause, passing in the $numRows parameter (as a placeholder), so that we can optionally limit the number of records returned. Finally, the special MySQL value SQL_CALC_FOUND_ROWS tells MySQL to return the actual number of records returned; this information is useful for displaying to the user, as well as for other things like pagination of results. Rather than pass the $order value to the query via a placeholder, we interpolate it directly into the query string itself, calling mysql_escape_string() to ensure that any special characters in the value are escaped (for security reasons). If we used a placeholder then PDO would place quotes (') around the string value (for example, ORDER BY 'publicationDate DESC'), which is invalid syntax. $list = array(); while ( $row = $st->fetch() ) { $article = new Article( $row ); $list[] = $article; } Since we're returning multiple rows, we create an array, $list, to hold the corresponding Article objects. We then use a while loop to retrieve the next row via fetch(), create a new Article object, store the row values in the object, and add the object to the $list array. When there are no more rows, fetch() returns false and the loop exits. // Now get the total number of articles that matched the criteria $sql = "SELECT FOUND_ROWS() AS totalRows"; $totalRows = $conn->query( $sql )->fetch(); $conn = null; return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) ); Finally, we run another query that uses the MySQL FOUND_ROWS() function to get the number of returned rows calculated by our previous SQL_CALC_FOUND_ROWS command. This time we use the PDO query() method, which lets us quickly run a query if there are no placeholders to bind. We call fetch() on the resulting statement handle to retrieve the result row, then return both the list of Article objects ($list) and the total row count as an associative array.

6. insert()

The remaining methods in our Article class deal with adding, changing and deleting article records in the database. insert() adds a new article record to the articles table, using the values stored in the current Article object: First, the method makes sure that the object doesn't already have its $id property set. If it does have an ID then the article presumably already exists in the database, so we shouldn't try to insert it again. Then the method runs an SQL INSERT query to insert the record into the articles table, using placeholders to pass the property values to the database. Note the use of the MySQL FROM_UNIXTIME() function to convert the publication date from UNIX timestamp format back into MySQL format. After running the query, the method retrieves the new article record's ID using the PDO lastInsertId() function, and stores it in the object's $id property for future reference. Remember that we set up the articles table's id field as an auto_increment field, so that MySQL generates a new unique ID for each new article record. Notice that we use PDO::PARAM_INT when binding integer values to placeholders, and PDO::PARAM_STR when binding string values. This is so that PDO can escape the values appropriately.

7. update()

This method is similar to insert(), except that it updates an existing article record in the database instead of creating a new record. First it checks that the object has an ID, since you can't update a record without knowing its ID. Then it uses the SQL UPDATE statement to update the record's fields. Notice that we pass the object's ID to the UPDATE statement so that it knows which record to update.

5. delete()

The delete() method is pretty self-explanatory. It uses the SQL DELETE statement to remove the article stored in the object from the articles table, using the object's $id property to identify the record in the table. For safety reasons, we add LIMIT 1 to the query to make sure that only 1 article record can be deleted at a time.

Step 5: Write the front-end index.php script

We've now created our Article class, which does the heavy lifting for our CMS. Now that's out of the way, the rest of the code is pretty simple! First, let's create index.php, the script that controls the display of the front-end pages of the site. Save this file in the cms folder you created earlier, at the start of Step 4: <?php require( "config.php" ); $action = isset( $_GET['action'] ) ? $_GET['action'] : ""; switch ( $action ) { case 'archive': archive(); break; case 'viewArticle': viewArticle(); break; default: homepage(); } function archive() { $results = array(); $data = Article::getList(); $results['articles'] = $data['results']; $results['totalRows'] = $data['totalRows']; $results['pageTitle'] = "Article Archive | Widget News"; require( TEMPLATE_PATH . "/archive.php" ); } function viewArticle() { if ( !isset($_GET["articleId"]) || !$_GET["articleId"] ) { homepage(); return; } $results = array(); $results['article'] = Article::getById( (int)$_GET["articleId"] ); $results['pageTitle'] = $results['article']->title . " | Widget News"; require( TEMPLATE_PATH . "/viewArticle.php" ); } function homepage() { $results = array(); $data = Article::getList( HOMEPAGE_NUM_ARTICLES ); $results['articles'] = $data['results']; $results['totalRows'] = $data['totalRows']; $results['pageTitle'] = "Widget News"; require( TEMPLATE_PATH . "/homepage.php" ); } ?> Let's break this script down: Include the config file The first line of code includes the config.php file we created earlier, so that all the configuration settings are available to the script. We use require() rather than include(); require() generates an error if the file can't be found. Grab the action parameter We store the $_GET['action'] parameter in a variable called $action, so that we can use the value later in the script. Before doing this, we check that the $_GET['action'] value exists by using isset(). If it doesn't, we set the corresponding $action variable to an empty string (""). It's good programming practice to check that user-supplied values, such as query string parameters, form post values and cookies, actually exist before attempting to use them. Not only does it limit security holes, but it prevents the PHP engine raising "undefined index" notices as your script runs. Decide which action to perform The switch block looks at the action parameter in the URL to determine which action to perform (display the archive, or view an article). If no action parameter is in the URL then the script displays the site homepage. archive() This function displays a list of all the articles in the database. It does this by calling the getList() method of the Article class that we created earlier. The function then stores the results, along with the page title, in a $results associative array so the template can display them in the page. Finally, it includes the template file to display the page. (We'll create the templates in a moment.) viewArticle() This function displays a single article page. It retrieves the ID of the article to display from the articleId URL parameter, then calls the Article class's getById() method to retrieve the article object, which it stores in the $results array for the template to use. (If no articleId was supplied, or the article couldn't be found, then the function simply displays the homepage instead.) Notice that we use (int) to cast the value of the articleID query parameter to an integer. This is a good security measure, as it prevents anything other than integers from being passed to our code. homepage() Our last function, homepage(), displays the site homepage containing a list of up to HOMEPAGE_NUM_ARTICLES articles (5 by default). It's much like the archive() function, except that it passes HOMEPAGE_NUM_ARTICLES to the getList() method to limit the number of articles returned.

Step 6: Write the back-end admin.php script

Our admin script is a bit more complex than index.php, since it deals with all the admin functions for the CMS. The basic structure, though, is similar to index.php. Save this file, admin.php, in the same folder as your index.php script: <?php require( "config.php" ); session_start(); $action = isset( $_GET['action'] ) ? $_GET['action'] : ""; $username = isset( $_SESSION['username'] ) ? $_SESSION['username'] : ""; if ( $action != "login" && $action != "logout" && !$username ) { login(); exit; } switch ( $action ) { case 'login': login(); break; case 'logout': logout(); break; case 'newArticle': newArticle(); break; case 'editArticle': editArticle(); break; case 'deleteArticle': deleteArticle(); break; default: listArticles(); } function login() { $results = array(); $results['pageTitle'] = "Admin Login | Widget News"; if ( isset( $_POST['login'] ) ) { // User has posted the login form: attempt to log the user in if ( $_POST['username'] == ADMIN_USERNAME && $_POST['password'] == ADMIN_PASSWORD ) { // Login successful: Create a session and redirect to the admin homepage $_SESSION['username'] = ADMIN_USERNAME; header( "Location: admin.php" ); } else { // Login failed: display an error message to the user $results['errorMessage'] = "Incorrect username or password. Please try again."; require( TEMPLATE_PATH . "/admin/loginForm.php" ); } } else { // User has not posted the login form yet: display the form require( TEMPLATE_PATH . "/admin/loginForm.php" ); } } function logout() { unset( $_SESSION['username'] ); header( "Location: admin.php" ); } function newArticle() { $results = array(); $results['pageTitle'] = "New Article"; $results['formAction'] = "newArticle"; if ( isset( $_POST['saveChanges'] ) ) { // User has posted the article edit form: save the new article $article = new Article; $article->storeFormValues( $_POST ); $article->insert(); header( "Location: admin.php?status=changesSaved" ); } elseif ( isset( $_POST['cancel'] ) ) { // User has cancelled their edits: return to the article list header( "Location: admin.php" ); } else { // User has not posted the article edit form yet: display the form $results['article'] = new Article; require( TEMPLATE_PATH . "/admin/editArticle.php" ); } } function editArticle() { $results = array(); $results['pageTitle'] = "Edit Article"; $results['formAction'] = "editArticle"; if ( isset( $_POST['saveChanges'] ) ) { // User has posted the article edit form: save the article changes if ( !$article = Article::getById( (int)$_POST['articleId'] ) ) { header( "Location: admin.php?error=articleNotFound" ); return; } $article->storeFormValues( $_POST ); $article->update(); header( "Location: admin.php?status=changesSaved" ); } elseif ( isset( $_POST['cancel'] ) ) { // User has cancelled their edits: return to the article list header( "Location: admin.php" ); } else { // User has not posted the article edit form yet: display the form $results['article'] = Article::getById( (int)$_GET['articleId'] ); require( TEMPLATE_PATH . "/admin/editArticle.php" ); } } function deleteArticle() { if ( !$article = Article::getById( (int)$_GET['articleId'] ) ) { header( "Location: admin.php?error=articleNotFound" ); return; } $article->delete(); header( "Location: admin.php?status=articleDeleted" ); } function listArticles() { $results = array(); $data = Article::getList(); $results['articles'] = $data['results']; $results['totalRows'] = $data['totalRows']; $results['pageTitle'] = "All Articles"; if ( isset( $_GET['error'] ) ) { if ( $_GET['error'] == "articleNotFound" ) $results['errorMessage'] = "Error: Article not found."; } if ( isset( $_GET['status'] ) ) { if ( $_GET['status'] == "changesSaved" ) $results['statusMessage'] = "Your changes have been saved."; if ( $_GET['status'] == "articleDeleted" ) $results['statusMessage'] = "Article deleted."; } require( TEMPLATE_PATH . "/admin/listArticles.php" ); } ?> Let's look at some interesting sections of this script: Start a user session Towards the top of the script we call session_start(). This PHP function starts a new session for the user, which we can use to track whether the user is logged in or not. (If a session for this user already exists, PHP automatically picks it up and uses it.) Because sessions need cookies to work, and cookies are sent to the browser before content, you should call session_start() at the top of the script, before any content has been output. Grab the action parameter and username session variable Next we store the $_GET['action'] parameter in a variable called $action, and the $_SESSION['username'] session variable in $username, so that we can use these values later in the script. Before doing this, we check that these values exist by using isset(). If a value doesn't exist then we set the corresponding variable to an empty string (""). Check the user is logged in The user shouldn't be allowed to do anything unless they're logged in as an administrator. So the next thing we do is inspect $username to see if the session contained a value for the username key, which we use to signify that the user is logged in. If $username's value is empty - and the user isn't already trying to log in or out - then we display the login page and exit immediately. Decide which action to perform The switch block works much like the one in index.php: it calls the appropriate function based on the value of the action URL parameter. The default action is to display the list of articles in the CMS. login() This is called when the user needs to log in, or is in the process of logging in. If the user has submitted the login form - which we check by looking for the login form parameter - then the function checks the entered username and password against the config values ADMIN_USERNAME and ADMIN_PASSWORD. If they match then the username session key is set to the admin username, effectively logging them in, and we then redirect the browser back to the admin.php script, which then displays the list of articles. If the username and password don't match then the login form is redisplayed with an error message. If the user hasn't submitted the login form yet then the function simply displays the form. logout() This function is called when the user elects to log out. It simply removes the username session key and redirects back to admin.php. newArticle() This function lets the user create a new article. If the user has just posted the "new article" form then the function creates a new Article object, stores the form data in the object by calling storeFormValues(), inserts the article into the database by calling insert(), and redirects back to the article list, displaying a "Changes Saved" status message. If the user has not posted the "new article" form yet then the function creates a new empty Article object with no values, then uses the editArticle.php template to display the article edit form using this empty Article object. editArticle() This function is similar to newArticle(), except that it lets the user edit an existing article. When the user saves their changes then the function retrieves the existing article using getById(), stores the new values in the Article object, then saves the changed object by calling update(). (If the article isn't found in the database then the function displays an error.) When displaying the article edit form, the function again uses the getById() method to load the current article field values into the form for editing. Notice that the script uses the same template (editArticle.php) both for creating new articles, and for editing existing articles. This means that we only need to create a single HTML form. The formAction parameter is used to determine if the user is adding or editing an article. deleteArticle() If the user has chosen to delete an article then this function first retrieves the article to be deleted (displaying an error if the article couldn't be found in the database), then calls the article's delete() method to remove the article from the database. It then redirects to the article list page, displaying an "article deleted" status message. listArticles() The last function in admin.php displays a list of all articles in the CMS for editing. The function uses the Article class's getList() method to retrieve all the articles, then it uses the listArticles.php template to display the list. Along the way, it also checks the URL query parameters error and status to see if any error or status message needs to be displayed in the page. If so, then it creates the necessary message and passes it to the template for display.

Step 7: Create the front-end templates

We've now created all the PHP code for our CMS's functionality. The next step is to create the HTML templates for both the front-end and admin pages. First, the front-end templates.

1. The include files

Create a folder called templates inside your cms folder. Now create a folder called include inside the templates folder. In this folder we're going to put the header and footer markup that is common to every page of the site, to save having to put it inside every template file. Create a new file called header.php inside your include folder, with the following code: <!DOCTYPE html> <html lang="en"> <head> <title><?php echo htmlspecialchars( $results['pageTitle'] )?></title> <link rel="stylesheet" type="text/css" href="style.css" /> </head> <body> <div> <a href="."><img src="images/logo.jpg" alt="Widget News" /></a> As you can see, this code simply displays the markup to start the HTML page. It uses the $results['pageTitle'] variable passed from the main script (index.php or admin.php) to set the title element, and also links to a stylesheet, style.css (we'll create this in a moment). Notice that we've passed the value of $results['pageTitle'] through the PHP function htmlspecialchars(). This function encodes any special HTML characters, such as <, >, and &, into their HTML entity equivalents (&lt;, &gt;, and &amp;). Along with filtering input - which we did when we wrote the Article constructor in Step 4 - encoding output is a good security habit to get into. We'll encode the majority of the data in our templates this way. Next, create a file called footer.php in the same folder: <div> Widget News &copy; 2011. All rights reserved. <a href="admin.php">Site Admin</a> </div> </div> </body> </html> This markup finishes off each HTML page in the system.

2. homepage.php

Now go back up to the templates folder, and create a homepage.php template file in there, with the following code: <?php include "templates/include/header.php" ?> <ul> <?php foreach ( $results['articles'] as $article ) { ?> <li> <h3> <span class="pubDate"><?php echo date('j F', $article->publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a> </h3> <p class="summary"><?php echo htmlspecialchars( $article->summary )?></p> </li> <?php } ?> </ul> <p><a href="./?action=archive">Article Archive</a></p> <?php include "templates/include/footer.php" ?> This template displays the article headlines on the homepage as an unordered list. It loops through the array of Article objects stored in $results['articles'] and displays each article's publication date, title, and summary. The title is linked back to '.' (index.php), passing action=viewArticle, as well as the article's ID, in the URL. This allows the visitor to read an article by clicking its title. The template also includes a link to the article archive ("./?action=archive"). Notice that this template, as well as subsequent templates, use the PHP include statement to include the header and footer files in the page.

3. archive.php

Now create an archive.php template file in your templates folder: <?php include "templates/include/header.php" ?> <h1>Article Archive</h1> <ul class="archive"> <?php foreach ( $results['articles'] as $article ) { ?> <li> <h3> <span class="pubDate"><?php echo date('j F Y', $article->publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a> </h3> <p class="summary"><?php echo htmlspecialchars( $article->summary )?></p> </li> <?php } ?> </ul> <p><?php echo $results['totalRows']?> article<?php echo ( $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p> <p><a href="./">Return to Homepage</a></p> <?php include "templates/include/footer.php" ?> This template displays the archive of all articles in the CMS. As you can see, it's almost identical to homepage.php. It adds an archive CSS class to the unordered list so we can style the list items a bit differently to the homepage, and it also adds the year to the article publication dates (since the archive might go back a few years). The page also includes a total count of the articles in the database, retrieved via $results['totalRows']. Finally, instead of the archive link at the bottom of the page, it includes a "Return to Homepage" link.

4. viewArticle.php

The last front-end template displays an article to the user. Create a file called viewArticle.php in your templates folder, and add the following markup: <?php include "templates/include/header.php" ?> <h1 style="width: 75%;"><?php echo htmlspecialchars( $results['article']->title )?></h1> <div style="width: 75%; font-style: italic;"><?php echo htmlspecialchars( $results['article']->summary )?></div> <div style="width: 75%;"><?php echo $results['article']->content?></div> <p class="pubDate">Published on <?php echo date('j F Y', $results['article']->publicationDate)?></p> <p><a href="./">Return to Homepage</a></p> <?php include "templates/include/footer.php" ?> This template is very straightforward. It displays the selected article's title, summary and content, as well as its publication date and a link to return to the homepage. You might have noticed that we haven't passed $results['article']->content through htmlspecialchars(). As explained when we created the Article constructor in Step 4, the administrator will probably want to use HTML markup, such as <p> tags, in the article content. If we encoded the content then <p> tags would appear on the page as <p>, rather than creating paragraphs.

Step 8: Create the back-end templates

Now that we've created the templates for the front end of the site, it's time to create the 3 admin templates.

1. loginForm.php

First, create another folder called admin inside your templates folder. Inside the admin folder, create the first of the 3 templates, loginForm.php: <?php include "templates/include/header.php" ?> <form action="admin.php?action=login" method="post" style="width: 50%;"> <input type="hidden" name="login" value="true" /> <?php if ( isset( $results['errorMessage'] ) ) { ?> <div class="errorMessage"><?php echo $results['errorMessage'] ?></div> <?php } ?> <ul> <li> <label for="username">Username</label> <input type="text" name="username" placeholder="Your admin username" required autofocus maxlength="20" /> </li> <li> <label for="password">Password</label> <input type="password" name="password" placeholder="Your admin password" required maxlength="20" /> </li> </ul> <div class="buttons"> <input type="submit" name="login" value="Login" /> </div> </form> <?php include "templates/include/footer.php" ?> This page contains the admin login form, which posts back to admin.php?action=login. It includes a hidden field, login, that our login() function from Step 6 uses to check if the form has been posted. The form also contains an area for displaying any error messages (such as an incorrect username or password), as well as username and password fields and a "Login" button. We've used some HTML5 form features such as placeholder, required, autofocus and date in our admin forms. This makes the forms nicer to use, and also saves us having to check for required fields in our PHP code. Since not all browsers currently support these HTML5 form features, you would probably want to use JavaScript and/or PHP fallbacks to check for required fields in a production system.

2. listArticles.php

Now create the second admin template in your admin folder. This one's called listArticles.php: <?php include "templates/include/header.php" ?> <div> <h3>Widget News Admin</h3> <p>You are logged in as <b><?php echo htmlspecialchars( $_SESSION['username']) ?></b>. <a href="admin.php?action=logout"?>Log out</a></p> </div> <h1>All Articles</h1> <?php if ( isset( $results['errorMessage'] ) ) { ?> <div class="errorMessage"><?php echo $results['errorMessage'] ?></div> <?php } ?> <?php if ( isset( $results['statusMessage'] ) ) { ?> <div class="statusMessage"><?php echo $results['statusMessage'] ?></div> <?php } ?> <table> <tr> <th>Publication Date</th> <th>Article</th> </tr> <?php foreach ( $results['articles'] as $article ) { ?> <tr onclick="location='admin.php?action=editArticle&amp;articleId=<?php echo $article->id?>'"> <td><?php echo date('j M Y', $article->publicationDate)?></td> <td> <?php echo $article->title?> </td> </tr> <?php } ?> </table> <p><?php echo $results['totalRows']?> article<?php echo ( $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p> <p><a href="admin.php?action=newArticle">Add a New Article</a></p> <?php include "templates/include/footer.php" ?> This template displays the list of articles for the administrator to edit. After displaying any error or status messages, it loops through the array of Article objects stored in $results['articles'], displaying each article's publication date and title in a table row. It also adds a JavaScript onclick event to each article's table row, so that the administrator can click an article to edit it. The template also includes the total article count, as well as a link to let the administrator add a new article.

3. editArticle.php

Now save the final template, editArticle.php, in your admin folder: <?php include "templates/include/header.php" ?> <div> <h3>Widget News Admin</h3> <p>You are logged in as <b><?php echo htmlspecialchars( $_SESSION['username']) ?></b>. <a href="admin.php?action=logout"?>Log out</a></p> </div> <h1><?php echo $results['pageTitle']?></h1> <form action="admin.php?action=<?php echo $results['formAction']?>" method="post"> <input type="hidden" name="articleId" value="<?php echo $results['article']->id ?>"/> <?php if ( isset( $results['errorMessage'] ) ) { ?> <div class="errorMessage"><?php echo $results['errorMessage'] ?></div> <?php } ?> <ul> <li> <label for="title">Article Title</label> <input type="text" name="title" placeholder="Name of the article" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $results['article']->title )?>" /> </li> <li> <label for="summary">Article Summary</label> <textarea name="summary" placeholder="Brief description of the article" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars( $results['article']->summary )?></textarea> </li> <li> <label for="content">Article Content</label> <textarea name="content" placeholder="The HTML content of the article" required maxlength="100000" style="height: 30em;"><?php echo htmlspecialchars( $results['article']->content )?></textarea> </li> <li> <label for="publicationDate">Publication Date</label> <input type="date" name="publicationDate" placeholder="YYYY-MM-DD" required maxlength="10" value="<?php echo $results['article']->publicationDate ? date( "Y-m-d", $results['article']->publicationDate ) : "" ?>" /> </li> </ul> <div class="buttons"> <input type="submit" name="saveChanges" value="Save Changes" /> <input type="submit" formnovalidate name="cancel" value="Cancel" /> </div> </form> <?php if ( $results['article']->id ) { ?> <p><a href="admin.php?action=deleteArticle&amp;articleId=<?php echo $results['article']->id ?>" onclick="return confirm('Delete This Article?')">Delete This Article</a></p> <?php } ?> <?php include "templates/include/footer.php" ?> This edit form is used both for creating new articles, and for editing existing articles. It posts to either admin.php?action=newArticle or admin.php?action=editArticle, depending on the value passed in the $results['formAction'] variable. It also contains a hidden field, articleId, to track the ID of the article being edited (if any). The form also includes an area for error messages, as well as fields for the article title, summary, content, and publication date. Finally, there are 2 buttons for saving and cancelling changes, and a link to allow the admin to delete the currently-edited article. As usual, we pass all data through htmlspecialchars() before outputting it in the markup. Not only is this a good security habit, but it also ensures that our form field values are properly escaped. For example, if the title field value contained a double quote (") that wasn't escaped then the title would be truncated, since double quotes are used to delimit the field's value in the markup. Note the use of the HTML5 formnovalidate attribute on the "Cancel" button. This handy attribute tells the browser not to validate the form if the user presses "Cancel".

Step 9: Create the stylesheet and logo image

Our CMS application is basically done now, but in order to make it look a bit nicer for both our visitors and the site administrator, we'll create a CSS file to control the look of the site. Save this file as style.css in your cms folder: /* Style the body and outer container */ body { margin: 0; color: #333; background-color: #00a0b0; font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; line-height: 1.5em; } #container { width: 960px; background: #fff; margin: 20px auto; padding: 20px; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; } /* The logo and footer */ #logo { display: block; width: 300px; padding: 0 660px 20px 0; border: none; border-bottom: 1px solid #00a0b0; margin-bottom: 40px; } #footer { border-top: 1px solid #00a0b0; margin-top: 40px; padding: 20px 0 0 0; font-size: .8em; } /* Headings */ h1 { color: #eb6841; margin-bottom: 30px; line-height: 1.2em; } h2, h2 a { color: #edc951; } h2 a { text-decoration: none; } /* Article headlines */ #headlines { list-style: none; padding-left: 0; width: 75%; } #headlines li { margin-bottom: 2em; } .pubDate { font-size: .8em; color: #eb6841; text-transform: uppercase; } #headlines .pubDate { display: inline-block; width: 100px; font-size: .5em; vertical-align: middle; } #headlines.archive .pubDate { width: 130px; } .summary { padding-left: 100px; } #headlines.archive .summary { padding-left: 130px; } /* "You are logged in..." header on admin pages */ #adminHeader { width: 940px; padding: 0 10px; border-bottom: 1px solid #00a0b0; margin: -30px 0 40px 0; font-size: 0.8em; } /* Style the form with a coloured background, along with curved corners and a drop shadow */ form { margin: 20px auto; padding: 40px 20px; overflow: auto; background: #fff4cf; border: 1px solid #666; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8); -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8); box-shadow: 0 0 .5em rgba(0, 0, 0, .8); } /* Give form elements consistent margin, padding and line height */ form ul { list-style: none; margin: 0; padding: 0; } form ul li { margin: .9em 0 0 0; padding: 0; } form * { line-height: 1em; } /* The field labels */ label { display: block; float: left; clear: left; text-align: right; width: 15%; padding: .4em 0 0 0; margin: .15em .5em 0 0; } /* The fields */ input, select, textarea { display: block; margin: 0; padding: .4em; width: 80%; } input, textarea, .date { border: 2px solid #666; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; background: #fff; } input { font-size: .9em; } select { padding: 0; margin-bottom: 2.5em; position: relative; top: .7em; } textarea { font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; font-size: .9em; height: 5em; line-height: 1.5em; } textarea#content { font-family: "Courier New", courier, fixed; } /* Place a border around focused fields */ form *:focus { border: 2px solid #7c412b; outline: none; } /* Display correctly filled-in fields with a green background */ input:valid, textarea:valid { background: #efe; } /* Submit buttons */ .buttons { text-align: center; margin: 40px 0 0 0; } input[type="submit"] { display: inline; margin: 0 20px; width: 12em; padding: 10px; border: 2px solid #7c412b; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8); -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8); box-shadow: 0 0 .5em rgba(0, 0, 0, .8); color: #fff; background: #ef7d50; font-weight: bold; -webkit-appearance: none; } input[type="submit"]:hover, input[type="submit"]:active { cursor: pointer; background: #fff; color: #ef7d50; } input[type="submit"]:active { background: #eee; -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset; -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset; box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset; } /* Tables */ table { width: 100%; border-collapse: collapse; } tr, th, td { padding: 10px; margin: 0; text-align: left; } table, th { border: 1px solid #00a0b0; } th { border-left: none; border-right: none; background: #ef7d50; color: #fff; cursor: default; } tr:nth-child(odd) { background: #fff4cf; } tr:nth-child(even) { background: #fff; } tr:hover { background: #ddd; cursor: pointer; } /* Status and error boxes */ .statusMessage, .errorMessage { font-size: .8em; padding: .5em; margin: 2em 0; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8); -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8); -box-shadow: 0 0 .5em rgba(0, 0, 0, .8); } .statusMessage { background-color: #2b2; border: 1px solid #080; color: #fff; } .errorMessage { background-color: #f22; border: 1px solid #800; color: #fff; } I won't go into the details of the CSS, since this tutorial is about PHP and MySQL! Suffice to say, it styles things like the page layout, colours, fonts, forms, tables and so on. Last, but not least, our site needs a logo. Here's one I prepared earlier - save it in an images folder inside your cms folder, calling it logo.jpg (or roll your own logo):

All done!

We've finished our CMS! To try it out, open a browser and point it to the base URL of your CMS (for example, http://localhost/cms/). Click the Site Admin link in the footer, log in, and add some articles. Then try browsing them on the front end (click the logo to return to the homepage). Don't forget you can try out the demo on our server too! In this tutorial you've built a basic content management system from the ground up, using PHP and MySQL. You've learnt about MySQL, tables, field types, PDO, object-oriented programming, templating, security, sessions, and lots more. While this CMS is pretty basic, it has hopefully given you a starting point for building your own CMS-driven websites. Some features you might want to add include: Pagination on the article archive (front end) and article list (back end) so that the system can easily handle hundreds of articles A WYSIWYG editor for easier content editing An image upload facility (I've written a follow-up tutorial on adding an image upload feature to the CMS) A preview facility, so the admin can see how an article will look before publishing it Article categories and tags (I've written a follow-up tutorial on adding categories) Integration with Apache's mod_rewrite to create more human-friendly permalink URLs for the articles (find out how to do this) A user comments system